#ifndef _NAPITRAY_H
#define _NAPITRAY_H

#include <napi.h>

#include <thread>
#include <iostream>
#include <gtk/gtk.h>
#include <map>


#define THROW(error) Napi::TypeError::New( env, error ).ThrowAsJavaScriptException()

#define EMIT(event_name, ...) info.This().As<Napi::Object>().Get("emit").As<Napi::Function>().Call(info.This(), {Napi::String::New(info.Env(), event_name), __VA_ARGS__})

void _tray_click_cb(GtkStatusIcon *status_icon, GdkEventButton *event, gpointer user_data);
std::thread gtkThread;
int i = 0;

struct callback_info{
    void *origin;
    std::string event_name;
};
static void OnTrayClose(const Napi::CallbackInfo& info);

static void OnTrayUpdate(const Napi::CallbackInfo& info);

static void OnClickCallBack(Napi::Env env, Napi::Function emitter, void *userData);

class Tray : public Napi::ObjectWrap<Tray> {
    public:
        static Napi::Object Init(Napi::Env env, Napi::Object exports){
            Napi::Function func =
                Tray::DefineClass(env, "Tray", {
                    Tray::InstanceMethod("close", &Tray::Close),
                    Tray::InstanceAccessor("icon", &Tray::GetIcon, &Tray::SetIcon),
                    Tray::InstanceAccessor("tooltip", &Tray::GetTooltip, &Tray::SetTooltip)});
            
            Napi::Function EventEmitter = env.Global().Get("EventEmitter").As<Napi::Function>();
            if( EventEmitter.IsEmpty() ){
                THROW("Expected EventEmitter constructor on global object.");
                return exports;
            }
            // Tray extends EventEmitter
            func.Get("prototype").As<Napi::Object>().Set("__proto__", EventEmitter.Get("prototype"));
            
            Napi::FunctionReference* constructor = new Napi::FunctionReference();
            *constructor = Napi::Persistent(func);
            env.SetInstanceData(constructor);
            return func;
        }

        Tray(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Tray>(info) {
            Napi::Env env = info.Env();
            if( info.Length() < 1 ){
                THROW("Expected argument \"icon\": string.");
                return;
            }
            tooltip = "杭密钉";
            icon = "";
            
            Napi::Function Emit = info.This().As<Napi::Object>().Get("emit").As<Napi::Function>();
            onClickCallback = Napi::ThreadSafeFunction::New(env, Emit, "emit-click", 0, 1);


           
            info.This().As<Napi::Object>()
                .Get("on").As<Napi::Function>()
                .Call(info.This(), {
                    Napi::String::New(info.Env(), "update")
                    , Napi::Function::New<OnTrayUpdate>(info.Env(), nullptr, this)
                    });

            info.This().As<Napi::Object>()
                .Get("on").As<Napi::Function>()
                .Call(info.This(), {
                    Napi::String::New(info.Env(), "close")
                    , Napi::Function::New<OnTrayClose>(info.Env(), nullptr, this)
                    });

            
            SetIcon(info, info[0].As<Napi::Value>());

            sprintf(id, "tray-indicator-id-%04d", i);
            i++;
            tray_icon = gtk_status_icon_new();
            // 加载图标文件到 GdkPixbuf 对象
            // auto pixbuf = gdk_pixbuf_new_from_file(icon.c_str(), nullptr);
            //add cache
            auto pixbuf = fetchPixbuf(icon);

            // 设置托盘图标
            gtk_status_icon_set_from_pixbuf(tray_icon, pixbuf);
            gtk_status_icon_set_visible(tray_icon, true);
            gtk_status_icon_set_tooltip_text(tray_icon, tooltip.c_str());
            g_signal_connect(G_OBJECT(tray_icon), "button-press-event", G_CALLBACK(_tray_click_cb), this);
            // g_signal_connect(G_OBJECT(tray_icon), "popup-menu", G_CALLBACK(_tray_right_click_cb), this);

            std::cout << "main thread : " << std::this_thread::get_id() << std::endl;
            gtkThread = std::thread([](){
                 std::cout << "gtkThread thread : " << std::this_thread::get_id() << std::endl;
                 gtk_main();
            });
        }

    public:
        GtkStatusIcon *tray_icon = nullptr;
        GtkStatusIcon *tray_icon2 = nullptr;
        char id[80] = "tray-indicator-id-nnnn";

        Napi::Value Close(const Napi::CallbackInfo& info){
            Napi::Function Emit = info.This().As<Napi::Object>().Get("emit").As<Napi::Function>();
            Emit.Call(info.This(), {Napi::String::New(info.Env(), "close")});
            this->Destroy();
            return info.Env().Undefined();
        }

        void SetIcon(const Napi::CallbackInfo& info, const Napi::Value& arg){
            Napi::Env env = info.Env();
            if( !arg.IsString() ){
                THROW("Expected argument \"icon\" to be a string.");
                return;
            }
            icon = arg.As<Napi::String>().Utf8Value();
            EMIT("update", Napi::String::New(info.Env(), "icon"));
        }

        Napi::Value GetIcon(const Napi::CallbackInfo& info){
            return Napi::String::New(info.Env(), icon);
        }

        void SetTooltip(const Napi::CallbackInfo& info, const Napi::Value& arg){
            Napi::Env env = info.Env();
            if( !arg.IsString() ){
                THROW("Expected argument \"tooltip\" to be a string.");
                return;
            }
            tooltip = arg.As<Napi::String>().Utf8Value();
            EMIT("update", Napi::String::New(info.Env(), "tooltip"));
        }

        Napi::Value GetTooltip(const Napi::CallbackInfo& info){
            return Napi::String::New(info.Env(), tooltip);
        }

        void Destroy(){
            onClickCallback.Release();
        }

        void Click(const std::string& event_name){
            struct callback_info *c = new struct callback_info();
            c->event_name = event_name;
            c->origin = (Tray*) this;
            onClickCallback.BlockingCall(c , OnClickCallBack);
        }

        // cache
        GdkPixbuf* fetchPixbuf(std::string &path){
            if(pixbufCache.find(path) != pixbufCache.end()){
                return pixbufCache[path];
            }

            auto pixbuf = gdk_pixbuf_new_from_file(icon.c_str(), nullptr);
            pixbufCache[path] = pixbuf;
            return pixbufCache[path];
        }

        std::string icon;
        std::string tooltip;

        Napi::ThreadSafeFunction onClickCallback;

        std::map<std::string , GdkPixbuf*> pixbufCache;
};


static void OnTrayClose(const Napi::CallbackInfo& info){
    Tray *t = (Tray *) info.Data();
    gtk_status_icon_set_visible(t->tray_icon, FALSE);
}

static void OnTrayUpdate(const Napi::CallbackInfo& info){
    if( info.Length() < 1 ){
        std::cerr << "Unknown update, without a argument.\n";
        return;
    }
    if( !info[0].IsString() ){
        std::cerr << "Unknown update, expected argument to be string. " << 
            info[0].ToString().Utf8Value() << "recieved.\n";
        return;
    }

    Tray *t = (Tray*) info.Data();
    if( !t->tray_icon ){
        return;
    }

    std::string updated = info[0].As<Napi::String>().Utf8Value();
    std::cout << "timer thread : " << std::this_thread::get_id() << std::endl;
    if( updated.compare("icon") == 0 ){
        g_idle_add([](gpointer data){
            Tray *t = (Tray *)data;
            std::cout << "g_idle_add thread : " << std::this_thread::get_id() << std::endl;
            // gtk_status_icon_set_visible(t->tray_icon, true);
            // gtk_status_icon_set_visible(t->tray_icon2, true);
            // GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(t->icon.c_str(), nullptr);

            auto pixbuf = t->fetchPixbuf(t->icon);
            gtk_status_icon_set_from_pixbuf(t->tray_icon, pixbuf);
            return 0;
        }, t);

    } else if( updated.compare("tooltip") == 0 ){
        gtk_status_icon_set_tooltip_text(t->tray_icon, t->tooltip.c_str());
    }
}

static void OnClickCallBack(Napi::Env env, Napi::Function emitter, void *userData){
    struct callback_info* c = (struct callback_info*) userData;
    Tray *data = (Tray *)c->origin;
    Napi::Object TrayInstance = data->Value().As<Napi::Object>();
    // printf("[%s]\n", c->event_name.c_str());
    emitter.Call(TrayInstance, {Napi::String::New(env, c->event_name.c_str())});
}


void _tray_click_cb(GtkStatusIcon *status_icon, GdkEventButton *event, gpointer user_data)
{
    Tray *t = (Tray *) user_data;
    std::string event_name = "click";
    if (event->button == 3) {
        event_name = "right-click";
    }
    t->Click(event_name);
}
 

void QuitGtk(void *){
    gtk_main_quit();
    gtkThread.join();
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    if( !gtk_init_check(0, NULL) ){
        Napi::Error::New(env, "GTK Init Check Failed").ThrowAsJavaScriptException();
        return env.Null().As<Napi::Object>();
    }

    napi_add_env_cleanup_hook(env, &QuitGtk, nullptr);

    return Tray::Init(env, exports);
}

NODE_API_MODULE(addon, Init)


#endif